Skip to content

feat: Enhance Course Optimizer Page with Previous Run Links and Improved UI #2356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

pganesh-apphelix
Copy link

@pganesh-apphelix pganesh-apphelix commented Aug 7, 2025

Description

This PR introduces multiple UI enhancements and new functionality for the Course Optimizer page. The key updates include:


Previous Run Link Section

  • New section added to display links from previous course runs.
  • This section is conditionally rendered based on a feature flag:
    contentstore.enable_course_optimizer_check_prev_run_links
    • Enabled: Displays the section.
    • Disabled: Section is hidden.
  • If enabled but no data is found, the section will display “No result found.”

Course Update, Handouts, and Custom Page Section

  • Added UI components to display links for:
    • Course Updates
    • Handouts
    • Custom Pages
  • These links will appear under both:
    • Broken Links section
    • Previous Run Links section

UI Enhancements

  • Updated UI for the following elements:
    • Scan Course button
    • Filter Links components
  • Applied consistent styling based on the new design specifications.
after-course-optimizer-enhancement-2025-08-08.mp4

Jira

Testing Instructions

Please verify the following:

  • 🔘 With the feature flag disabled, the Previous Run Link section does not appear.
  • ✅ With the feature flag enabled:
    • If no previous run link is available, the section displays “No result found.”
    • All relevant Course Update, Handouts, and Custom Page links should be visible in both the Broken Links and Previous Run Links sections.

Copy link

codecov bot commented Aug 8, 2025

Codecov Report

❌ Patch coverage is 97.64706% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.50%. Comparing base (7825bcd) to head (1dbf10e).

Files with missing lines Patch % Lines
src/optimizer-page/scan-results/ScanResults.tsx 97.05% 4 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master    #2356    +/-   ##
========================================
  Coverage   94.50%   94.50%            
========================================
  Files        1171     1171            
  Lines       25149    25272   +123     
  Branches     5374     5568   +194     
========================================
+ Hits        23766    23884   +118     
+ Misses       1320     1319     -1     
- Partials       63       69     +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines 10 to 38
const PreviousRunLinkHref: FC<{ href: string }> = ({ href }) => {
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
window.open(href, '_blank');
};

return (
<div className="broken-link-container">
<a href={href} onClick={handleClick} className="broken-link" rel="noreferrer">
{href}
</a>
</div>
);
};

const GoToBlock: FC<{ block: { url: string, displayName: string } }> = ({ block }) => {
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
window.open(block.url, '_blank');
};

return (
<div className="go-to-block-link-container">
<a href={block.url} onClick={handleClick} className="broken-link" rel="noreferrer">
{block.displayName}
</a>
</div>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why adding these functions again,
Can we import these from brokenLinkTable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, refactored the functions accordingly.

Comment on lines 86 to 98
<Card className="unit-card rounded-sm pt-2 pb-3 pl-3 pr-4 mb-2.5">
<p className="unit-header">{unit.displayName}</p>
<DataTable
data={previousRunLinkList}
itemCount={previousRunLinkList.length}
columns={[
{
accessor: 'Links',
width: 'col-12',
},
]}
/>
</Card>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part is also same, can we add changes to same file BrokenLinkTable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, refactored the same.

Comment on lines 35 to 47
const collapsibleTitle = (
<div className={className}>
<div className="section-collapsible-header-item">
<Icon src={isOpen ? ArrowDropDown : ArrowRight} />
<p className="section-title">{title}</p>
</div>
<div className="section-collapsible-header-actions">
<div className="section-collapsible-header-action-item">
<p>{previousRunLinksCount > 0 ? previousRunLinksCount : '-'}</p>
</div>
</div>
</div>
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use SectionCollapsible?? As only function arguments and collapsibleTitle is changing in both with same return value including a lot of duplications.

We can edit SectionCollapsible based on a flag i think instead of creating a new component.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, It is updated as per the suggestion.

Comment on lines 59 to 60
const virtualSections = useMemo(() => {
const createVirtualSection = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use some different name? Virtual isn't making any sense, what do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is updated with meaningful variable name.

Comment on lines 138 to 137
// eslint-disable-next-line max-len
() => allSectionsForPrevRun.some(section => section.subsections.some(subsection => subsection.units.some(unit => unit.blocks.some(block => block.previousRunLinks && block.previousRunLinks.length > 0)))),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we format this line instead of adding // eslint-disable-next-line max-len

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, It's updated.

Comment on lines 187 to 186
// eslint-disable-next-line max-len
const hasVisibleUnit = section.subsections.some((subsection) => subsection.units.some((unit) => unit.blocks.some((block) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. can we remove // eslint-disable-next-line max-len

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, It's updated.

subtitle={intl.formatMessage(messages.headingSubtitle)}
/>
<Card>
<div className="d-flex flex-wrap justify-content-between align-items-center mb-3 px-3 py-3">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding px-3 py-3, can we add p-3?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, It's updated.

size="md"
className="px-4 rounded-0 scan-course-btn"
onClick={() => dispatch(startLinkCheck(courseId))}
disabled={(!!linkCheckInProgress) && !errorMessage}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use linkCheckInProgress instead of !!linkCheckInProgress as it's a boolean field.

Copy link
Author

@pganesh-apphelix pganesh-apphelix Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need !! because linkCheckInProgress can be null, but disabled only accepts true or false. !! makes sure it’s a valid boolean and also needed for type check.

animation="border"
size="sm"
className="mr-2"
style={{ width: '1rem', height: '1rem' }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move this to scss file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, It's updated.

</div>
<Card style={{ boxShadow: 'none', backgroundColor: 'transparent' }}>
<p className="px-3 py-1 small">{intl.formatMessage(messages.description)}</p>
<hr style={{ margin: '0 20px' }} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here & everywhere else,
can we move this to scss file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, It's updated.

Comment on lines 107 to 123
if (linkType === 'previous') {
// Handle previous run links (no filtering, no icons)
if (block.previousRunLinks && block.previousRunLinks.length > 0) {
const blockPreviousRunLinks = block.previousRunLinks.map((link) => ({
Links: (
<LinksCol
block={{ url: block.url, displayName: block.displayName || 'Go to block' }}
href={link}
showIcon={false}
/>
),
}));
acc.push(...blockPreviousRunLinks);
}
} else {
// Handle broken links with filtering and icons
if (!filters) { return acc; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conditions in else block are same and in start we have just added early return which neglects/ignores || (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) in below if blocks.

I think we should remove else and just add if block for previousRunLinks and keep the other if blocks same. This will also reduce no.of lines git diff, which helps in reviewing faster.

if (linkType === 'previous') {
        // Handle previous run links (no filtering, no icons)
        if (block.previousRunLinks && block.previousRunLinks.length > 0) {
          const blockPreviousRunLinks = block.previousRunLinks.map((link) => ({
            Links: (
              <LinksCol
                block={{ url: block.url, displayName: block.displayName || 'Go to block' }}
                href={link}
                showIcon={false}
              />
            ),
          }));
          acc.push(...blockPreviousRunLinks);
        }
      }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, It's removed

Comment on lines 165 to 177
useEffect(() => {
setOpenStates(data?.sections ? data.sections.map(() => false) : []);
}, [data?.sections]);
if (!data?.sections) {
return <InfoCard text={intl.formatMessage(messages.noBrokenLinksCard)} />;
}
setOpenStates(allSectionsForBrokenLinks ? allSectionsForBrokenLinks.map(() => false) : []);
setPrevRunOpenStates(allSectionsForPrevRun ? allSectionsForPrevRun.map(() => false) : []);
}, [allSectionsForBrokenLinks, allSectionsForPrevRun]);

const { sections } = data;
if (!data) {
return <InfoCard text={intl.formatMessage(messages.noDataCard)} />;
}
Copy link
Contributor

@Faraz32123 Faraz32123 Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid refactoring previous code completely resulting in unwanted edge-case issues.
Cause data would still contain empty arrays, So incase even if we have empty arrays, !data would always be false? Correct me if I am wrong!

Screenshot 2025-08-15 at 2 27 09 PM

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is updated.

@pganesh-apphelix pganesh-apphelix force-pushed the pganesh/TNL2-141 branch 2 times, most recently from e4b049d to 24befc9 Compare August 18, 2025 11:18
Comment on lines 127 to 131
// Combine renderable sections with regular sections
const allSectionsForBrokenLinks = useMemo(
() => [...renderableSections, ...(sections || [])],
[renderableSections, sections],
);

const allSectionsForPrevRun = useMemo(
() => [...renderableSections, ...(sections || [])],
[renderableSections, sections],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we improve this? both seems same to me.

Comment on lines 153 to 149
// Calculate previous run links count for each section (including virtual sections)
const previousRunLinksCounts = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

virtual sections?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants